1
2
3
4
5
6
7
8
9
10
11
12
13 package org.apache.tapestry5.internal.services;
14
15 import org.apache.tapestry5.internal.parser.*;
16 import org.apache.tapestry5.ioc.Location;
17 import org.apache.tapestry5.ioc.Resource;
18 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
19 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
20 import org.apache.tapestry5.ioc.internal.util.TapestryException;
21 import org.apache.tapestry5.ioc.util.ExceptionUtils;
22
23 import javax.xml.namespace.QName;
24 import java.net.URL;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30
31 import static org.apache.tapestry5.internal.services.SaxTemplateParser.Version.*;
32
33
34
35
36
37
38
39
40
41
42
43
44
45 @SuppressWarnings(
46 {"JavaDoc"})
47 public class SaxTemplateParser
48 {
49 private static final String MIXINS_ATTRIBUTE_NAME = "mixins";
50
51 private static final String TYPE_ATTRIBUTE_NAME = "type";
52
53 private static final String ID_ATTRIBUTE_NAME = "id";
54
55 public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
56
57 private static final Map<String, Version> NAMESPACE_URI_TO_VERSION = CollectionFactory.newMap();
58
59 {
60 NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_0_0.xsd", T_5_0);
61 NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_1_0.xsd", T_5_1);
62
63
64
65 NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_3.xsd", T_5_3);
66
67
68 NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_4.xsd", T_5_4);
69 }
70
71
72
73
74
75
76 private static final String TAPESTRY_PARAMETERS_URI = "tapestry:parameter";
77
78
79
80
81
82 private static final String LIB_NAMESPACE_URI_PREFIX = "tapestry-library:";
83
84
85
86
87
88
89
90 private static final Pattern LIBRARY_PATH_PATTERN = Pattern.compile("^[a-z]\\w*(/[a-z]\\w*)*$",
91 Pattern.CASE_INSENSITIVE);
92
93 private static final Pattern ID_PATTERN = Pattern.compile("^[a-z]\\w*$",
94 Pattern.CASE_INSENSITIVE);
95
96
97
98
99
100
101
102 private static final Pattern REDUCE_LINEBREAKS_PATTERN = Pattern.compile(
103 "[ \\t\\f]*[\\r\\n]\\s*", Pattern.MULTILINE);
104
105
106
107
108
109
110 private static final Pattern REDUCE_WHITESPACE_PATTERN = Pattern.compile("[ \\t\\f]+",
111 Pattern.MULTILINE);
112
113
114
115
116
117
118 private static final Pattern EXPANSION_PATTERN = Pattern.compile("\\$\\{\\s*(((?!\\$\\{).)*)\\s*}");
119 private static final char EXPANSION_STRING_DELIMITTER = '\'';
120 private static final char OPEN_BRACE = '{';
121 private static final char CLOSE_BRACE = '}';
122
123 private static final Set<String> MUST_BE_ROOT = CollectionFactory.newSet("extend", "container");
124
125 private final Resource resource;
126
127 private final XMLTokenStream tokenStream;
128
129 private final StringBuilder textBuffer = new StringBuilder();
130
131 private final List<TemplateToken> tokens = CollectionFactory.newList();
132
133
134
135 private List<TemplateToken> tokenAccumulator = tokens;
136
137
138
139
140
141 private final Map<String, Location> componentIds = CollectionFactory.newCaseInsensitiveMap();
142
143
144
145
146
147
148 private Map<String, List<TemplateToken>> overrides;
149
150 private boolean extension;
151
152 private Location textStartLocation;
153
154 private boolean active = true;
155
156 private boolean strictMixinParameters = false;
157
158 private final Map<String, Boolean> extensionPointIdSet = CollectionFactory.newCaseInsensitiveMap();
159
160 public SaxTemplateParser(Resource resource, Map<String, URL> publicIdToURL)
161 {
162 this.resource = resource;
163 this.tokenStream = new XMLTokenStream(resource, publicIdToURL);
164 }
165
166 public ComponentTemplate parse(boolean compressWhitespace)
167 {
168 try
169 {
170 tokenStream.parse();
171
172 TemplateParserState initialParserState = new TemplateParserState()
173 .compressWhitespace(compressWhitespace);
174
175 root(initialParserState);
176
177 return new ComponentTemplateImpl(resource, tokens, componentIds, extension, strictMixinParameters, overrides);
178 } catch (Exception ex)
179 {
180 throw new TapestryException(String.format("Failure parsing template %s: %s", resource,
181 ExceptionUtils.toMessage(ex)), tokenStream.getLocation(), ex);
182 }
183
184 }
185
186 void root(TemplateParserState state)
187 {
188 while (active && tokenStream.hasNext())
189 {
190 switch (tokenStream.next())
191 {
192 case DTD:
193
194 dtd();
195
196 break;
197
198 case START_ELEMENT:
199
200 rootElement(state);
201
202 break;
203
204 case END_DOCUMENT:
205
206 break;
207
208 default:
209 textContent(state);
210 }
211 }
212 }
213
214 private void rootElement(TemplateParserState initialState)
215 {
216 TemplateParserState state = setupForElement(initialState);
217
218 String uri = tokenStream.getNamespaceURI();
219 String name = tokenStream.getLocalName();
220 Version version = NAMESPACE_URI_TO_VERSION.get(uri);
221
222 if (T_5_1.sameOrEarlier(version))
223 {
224 if (name.equalsIgnoreCase("extend"))
225 {
226 extend(state);
227 return;
228 }
229 }
230
231 if (version != null)
232 {
233 if (name.equalsIgnoreCase("container"))
234 {
235 container(state);
236 return;
237 }
238 }
239
240 element(state);
241 }
242
243 private void extend(TemplateParserState state)
244 {
245 extension = true;
246
247 while (active)
248 {
249 switch (tokenStream.next())
250 {
251 case START_ELEMENT:
252
253 if (isTemplateVersion(Version.T_5_1) && isElementName("replace"))
254 {
255 replace(state);
256 break;
257 }
258
259 boolean is54 = isTemplateVersion(Version.T_5_4);
260
261 if (is54 && isElementName("block"))
262 {
263 block(state);
264 break;
265 }
266
267 throw new RuntimeException(
268 is54
269 ? "Child element of <extend> must be <replace> or <block>."
270 : "Child element of <extend> must be <replace>.");
271
272 case END_ELEMENT:
273
274 return;
275
276
277
278 case COMMENT:
279 case SPACE:
280 break;
281
282
283
284 case CHARACTERS:
285 if (InternalUtils.isBlank(tokenStream.getText()))
286 break;
287
288 default:
289 unexpectedEventType();
290 }
291 }
292 }
293
294
295
296
297 private boolean isElementName(String elementName)
298 {
299 return tokenStream.getLocalName().equalsIgnoreCase(elementName);
300 }
301
302
303
304
305 private boolean isTemplateVersion(Version requiredVersion)
306 {
307 Version templateVersion = NAMESPACE_URI_TO_VERSION.get(tokenStream.getNamespaceURI());
308
309 return requiredVersion.sameOrEarlier(templateVersion);
310 }
311
312 private void replace(TemplateParserState state)
313 {
314 String id = getRequiredIdAttribute();
315
316 addContentToOverride(setupForElement(state), id);
317 }
318
319 private void unexpectedEventType()
320 {
321 XMLTokenType eventType = tokenStream.getEventType();
322
323 throw new IllegalStateException(String.format("Unexpected XML parse event %s.", eventType
324 .name()));
325 }
326
327 private void dtd()
328 {
329 DTDData dtdInfo = tokenStream.getDTDInfo();
330
331 tokenAccumulator.add(new DTDToken(dtdInfo.rootName, dtdInfo.publicId, dtdInfo
332 .systemId, getLocation()));
333 }
334
335 private Location getLocation()
336 {
337 return tokenStream.getLocation();
338 }
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365 void element(TemplateParserState initialState)
366 {
367 TemplateParserState state = setupForElement(initialState);
368
369 String uri = tokenStream.getNamespaceURI();
370 String name = tokenStream.getLocalName();
371 Version version = NAMESPACE_URI_TO_VERSION.get(uri);
372
373 if (T_5_1.sameOrEarlier(version))
374 {
375
376 if (name.equalsIgnoreCase("remove"))
377 {
378 removeContent();
379
380 return;
381 }
382
383 if (name.equalsIgnoreCase("content"))
384 {
385 limitContent(state);
386
387 return;
388 }
389
390 if (name.equalsIgnoreCase("extension-point"))
391 {
392 extensionPoint(state);
393
394 return;
395 }
396
397 if (name.equalsIgnoreCase("replace"))
398 {
399 throw new RuntimeException(
400 "The <replace> element may only appear directly within an extend element.");
401 }
402
403 if (MUST_BE_ROOT.contains(name))
404 mustBeRoot(name);
405 }
406
407 if (version != null)
408 {
409
410 if (name.equalsIgnoreCase("body"))
411 {
412 body();
413 return;
414 }
415
416 if (name.equalsIgnoreCase("container"))
417 {
418 mustBeRoot(name);
419 }
420
421 if (name.equalsIgnoreCase("block"))
422 {
423 block(state);
424 return;
425 }
426
427 if (name.equalsIgnoreCase("parameter"))
428 {
429 if (T_5_3.sameOrEarlier(version))
430 {
431 throw new RuntimeException(
432 String.format("The <parameter> element has been deprecated in Tapestry 5.3 in favour of '%s' namespace.", TAPESTRY_PARAMETERS_URI));
433 }
434
435 classicParameter(state);
436
437 return;
438 }
439
440 possibleTapestryComponent(state, null, tokenStream.getLocalName().replace('.', '/'));
441
442 return;
443 }
444
445 if (uri != null && uri.startsWith(LIB_NAMESPACE_URI_PREFIX))
446 {
447 libraryNamespaceComponent(state);
448
449 return;
450 }
451
452 if (TAPESTRY_PARAMETERS_URI.equals(uri))
453 {
454 parameterElement(state);
455
456 return;
457 }
458
459
460
461 possibleTapestryComponent(state, tokenStream.getLocalName(), null);
462 }
463
464
465
466
467
468
469
470
471
472 private void processBody(TemplateParserState state)
473 {
474 while (active)
475 {
476 switch (tokenStream.next())
477 {
478 case START_ELEMENT:
479
480
481
482 element(state);
483 break;
484
485 case END_ELEMENT:
486
487
488
489
490
491 endElement(state);
492
493 return;
494
495 default:
496 textContent(state);
497 }
498 }
499 }
500
501 private TemplateParserState setupForElement(TemplateParserState initialState)
502 {
503 processTextBuffer(initialState);
504
505 return checkForXMLSpaceAttribute(initialState);
506 }
507
508
509
510
511
512
513
514 private void extensionPoint(TemplateParserState state)
515 {
516
517
518
519
520
521 String id = getRequiredIdAttribute();
522
523 if (extensionPointIdSet.containsKey(id))
524 {
525 throw new TapestryException(String.format("Extension point '%s' is already defined for this template. Extension point ids must be unique.", id), getLocation(), null);
526 } else
527 {
528 extensionPointIdSet.put(id, true);
529 }
530
531 tokenAccumulator.add(new ExtensionPointToken(id, getLocation()));
532
533 addContentToOverride(state.insideComponent(false), id);
534 }
535
536 private String getRequiredIdAttribute()
537 {
538 String id = getSingleParameter("id");
539
540 if (InternalUtils.isBlank(id))
541 throw new RuntimeException(String.format("The <%s> element must have an id attribute.",
542 tokenStream.getLocalName()));
543
544 return id;
545 }
546
547 private void addContentToOverride(TemplateParserState state, String id)
548
549 {
550 List<TemplateToken> savedTokenAccumulator = tokenAccumulator;
551
552 tokenAccumulator = CollectionFactory.newList();
553
554
555
556
557
558 if (overrides == null)
559 overrides = CollectionFactory.newCaseInsensitiveMap();
560
561 overrides.put(id, tokenAccumulator);
562
563 while (active)
564 {
565 switch (tokenStream.next())
566 {
567 case START_ELEMENT:
568 element(state);
569 break;
570
571 case END_ELEMENT:
572
573 processTextBuffer(state);
574
575
576
577
578 tokenAccumulator = savedTokenAccumulator;
579 return;
580
581 default:
582 textContent(state);
583 }
584 }
585 }
586
587 private void mustBeRoot(String name)
588 {
589 throw new RuntimeException(String.format(
590 "Element <%s> is only valid as the root element of a template.", name));
591 }
592
593
594
595
596
597
598 private void limitContent(TemplateParserState state)
599 {
600 if (state.isCollectingContent())
601 throw new IllegalStateException(
602 "The <content> element may not be nested within another <content> element.");
603
604 TemplateParserState newState = state.collectingContent().insideComponent(false);
605
606
607
608 tokens.clear();
609
610
611
612
613
614
615 overrides = null;
616
617
618
619
620
621 tokenAccumulator = tokens;
622
623 while (active)
624 {
625 switch (tokenStream.next())
626 {
627 case START_ELEMENT:
628 element(newState);
629 break;
630
631 case END_ELEMENT:
632
633
634
635
636
637 processTextBuffer(newState);
638
639 active = false;
640
641 break;
642
643 default:
644 textContent(state);
645 }
646 }
647
648 }
649
650 private void removeContent()
651 {
652 int depth = 1;
653
654 while (active)
655 {
656 switch (tokenStream.next())
657 {
658 case START_ELEMENT:
659 depth++;
660 break;
661
662
663
664 case END_ELEMENT:
665 depth--;
666
667 if (depth == 0)
668 return;
669
670 break;
671
672 default:
673
674 }
675 }
676 }
677
678 private String nullForBlank(String input)
679 {
680 return InternalUtils.isBlank(input) ? null : input;
681 }
682
683
684
685
686 private void libraryNamespaceComponent(TemplateParserState state)
687 {
688 String uri = tokenStream.getNamespaceURI();
689
690
691
692 String path = uri.substring(LIB_NAMESPACE_URI_PREFIX.length());
693
694 if (!LIBRARY_PATH_PATTERN.matcher(path).matches())
695 throw new RuntimeException(String.format("The path portion of library namespace URI '%s' is not valid: it must be a simple identifier, or a series of identifiers seperated by slashes.", uri));
696
697 possibleTapestryComponent(state, null, path + "/" + tokenStream.getLocalName());
698 }
699
700
701
702
703
704
705
706 private void possibleTapestryComponent(TemplateParserState state, String elementName,
707 String identifiedType)
708 {
709 String id = null;
710 String type = identifiedType;
711 String mixins = null;
712
713 int count = tokenStream.getAttributeCount();
714
715 Location location = getLocation();
716
717 List<TemplateToken> attributeTokens = CollectionFactory.newList();
718
719 for (int i = 0; i < count; i++)
720 {
721 QName qname = tokenStream.getAttributeName(i);
722
723 if (isXMLSpaceAttribute(qname))
724 continue;
725
726
727
728 String localName = qname.getLocalPart();
729
730 if (InternalUtils.isBlank(localName))
731 continue;
732
733 String uri = qname.getNamespaceURI();
734
735 String value = tokenStream.getAttributeValue(i);
736
737
738 Version version = NAMESPACE_URI_TO_VERSION.get(uri);
739
740 if (version != null)
741 {
742
743
744
745 if (T_5_4.sameOrEarlier(version)) {
746 strictMixinParameters = true;
747 }
748
749 if (localName.equalsIgnoreCase(ID_ATTRIBUTE_NAME))
750 {
751 id = nullForBlank(value);
752
753 validateId(id, "Component id '%s' is not valid; component ids must be valid Java identifiers: start with a letter, and consist of letters, numbers and underscores.");
754
755 continue;
756 }
757
758 if (type == null && localName.equalsIgnoreCase(TYPE_ATTRIBUTE_NAME))
759 {
760 type = nullForBlank(value);
761 continue;
762 }
763
764 if (localName.equalsIgnoreCase(MIXINS_ATTRIBUTE_NAME))
765 {
766 mixins = nullForBlank(value);
767 continue;
768 }
769
770
771
772
773
774 }
775
776 attributeTokens.add(new AttributeToken(uri, localName, value, location));
777 }
778
779 boolean isComponent = (id != null || type != null);
780
781
782
783
784 if (mixins != null && !isComponent)
785 throw new TapestryException(String.format("You may not specify mixins for element <%s> because it does not represent a component (which requires either an id attribute or a type attribute).", elementName),
786 location, null);
787
788 if (isComponent)
789 {
790 tokenAccumulator.add(new StartComponentToken(elementName, id, type, mixins, location));
791 } else
792 {
793 tokenAccumulator.add(new StartElementToken(tokenStream.getNamespaceURI(), elementName,
794 location));
795 }
796
797 addDefineNamespaceTokens();
798
799 tokenAccumulator.addAll(attributeTokens);
800
801 if (id != null)
802 componentIds.put(id, location);
803
804 processBody(state.insideComponent(isComponent));
805 }
806
807 private void addDefineNamespaceTokens()
808 {
809 for (int i = 0; i < tokenStream.getNamespaceCount(); i++)
810 {
811 String uri = tokenStream.getNamespaceURI(i);
812
813
814
815
816 if (NAMESPACE_URI_TO_VERSION.containsKey(uri))
817 continue;
818
819 if (uri.equals(TAPESTRY_PARAMETERS_URI))
820 continue;
821
822 if (uri.startsWith(LIB_NAMESPACE_URI_PREFIX))
823 continue;
824
825 tokenAccumulator.add(new DefineNamespacePrefixToken(uri, tokenStream
826 .getNamespacePrefix(i), getLocation()));
827 }
828 }
829
830 private TemplateParserState checkForXMLSpaceAttribute(TemplateParserState state)
831 {
832 for (int i = 0; i < tokenStream.getAttributeCount(); i++)
833 {
834 QName qName = tokenStream.getAttributeName(i);
835
836 if (isXMLSpaceAttribute(qName))
837 {
838 boolean compress = !"preserve".equals(tokenStream.getAttributeValue(i));
839
840 return state.compressWhitespace(compress);
841 }
842 }
843
844 return state;
845 }
846
847
848
849
850 private void endElement(TemplateParserState state)
851 {
852 processTextBuffer(state);
853
854 tokenAccumulator.add(new EndElementToken(getLocation()));
855 }
856
857
858
859
860
861
862 private void classicParameter(TemplateParserState state)
863 {
864 String parameterName = getSingleParameter("name");
865
866 if (InternalUtils.isBlank(parameterName))
867 throw new TapestryException("The name attribute of the <parameter> element must be specified.",
868 getLocation(), null);
869
870 ensureParameterWithinComponent(state);
871
872 tokenAccumulator.add(new ParameterToken(parameterName, getLocation()));
873
874 processBody(state.insideComponent(false));
875 }
876
877 private void ensureParameterWithinComponent(TemplateParserState state)
878 {
879 if (!state.isInsideComponent())
880 throw new RuntimeException(
881 "Block parameters are only allowed directly within component elements.");
882 }
883
884
885
886
887
888 private void parameterElement(TemplateParserState state)
889 {
890 ensureParameterWithinComponent(state);
891
892 if (tokenStream.getAttributeCount() > 0)
893 throw new TapestryException("A block parameter element does not allow any additional attributes. The element name defines the parameter name.",
894 getLocation(), null);
895
896 tokenAccumulator.add(new ParameterToken(tokenStream.getLocalName(), getLocation()));
897
898 processBody(state.insideComponent(false));
899 }
900
901
902
903
904
905
906 private void body()
907 {
908 tokenAccumulator.add(new BodyToken(getLocation()));
909
910 while (active)
911 {
912 switch (tokenStream.next())
913 {
914 case END_ELEMENT:
915 return;
916
917 default:
918 throw new IllegalStateException(String.format("Content inside a Tapestry body element is not allowed (at %s). The content has been ignored.", getLocation()));
919 }
920 }
921 }
922
923
924
925
926
927
928
929 private void container(TemplateParserState state)
930 {
931 while (active)
932 {
933 switch (tokenStream.next())
934 {
935 case START_ELEMENT:
936 element(state);
937 break;
938
939
940
941
942 case END_ELEMENT:
943
944 processTextBuffer(state);
945
946 return;
947
948 default:
949 textContent(state);
950 }
951 }
952 }
953
954
955
956
957
958 private void block(TemplateParserState state)
959 {
960 String blockId = getSingleParameter("id");
961
962 validateId(blockId, "Block id '%s' is not valid; block ids must be valid Java identifiers: start with a letter, and consist of letters, numbers and underscores.");
963
964 tokenAccumulator.add(new BlockToken(blockId, getLocation()));
965
966 processBody(state.insideComponent(false));
967 }
968
969 private String getSingleParameter(String attributeName)
970 {
971 String result = null;
972
973 for (int i = 0; i < tokenStream.getAttributeCount(); i++)
974 {
975 QName qName = tokenStream.getAttributeName(i);
976
977 if (isXMLSpaceAttribute(qName))
978 continue;
979
980 if (qName.getLocalPart().equalsIgnoreCase(attributeName))
981 {
982 result = tokenStream.getAttributeValue(i);
983 continue;
984 }
985
986
987
988 throw new TapestryException(String.format("Element <%s> does not support an attribute named '%s'. The only allowed attribute name is '%s'.", tokenStream
989 .getLocalName(), qName.toString(), attributeName), getLocation(), null);
990 }
991
992 return result;
993 }
994
995 private void validateId(String id, String messageKey)
996 {
997 if (id == null)
998 return;
999
1000 if (ID_PATTERN.matcher(id).matches())
1001 return;
1002
1003
1004
1005 throw new TapestryException(String.format(messageKey, id), getLocation(), null);
1006 }
1007
1008 private boolean isXMLSpaceAttribute(QName qName)
1009 {
1010 return XML_NAMESPACE_URI.equals(qName.getNamespaceURI())
1011 && "space".equals(qName.getLocalPart());
1012 }
1013
1014
1015
1016
1017
1018
1019
1020
1021 private void textContent(TemplateParserState state)
1022 {
1023 switch (tokenStream.getEventType())
1024 {
1025 case COMMENT:
1026 comment(state);
1027 break;
1028
1029 case CDATA:
1030 cdata(state);
1031 break;
1032
1033 case CHARACTERS:
1034 case SPACE:
1035 characters();
1036 break;
1037
1038 default:
1039 unexpectedEventType();
1040 }
1041 }
1042
1043 private void characters()
1044 {
1045 if (textStartLocation == null)
1046 textStartLocation = getLocation();
1047
1048 textBuffer.append(tokenStream.getText());
1049 }
1050
1051 private void cdata(TemplateParserState state)
1052 {
1053 processTextBuffer(state);
1054
1055 tokenAccumulator.add(new CDATAToken(tokenStream.getText(), getLocation()));
1056 }
1057
1058 private void comment(TemplateParserState state)
1059 {
1060 processTextBuffer(state);
1061
1062 String comment = tokenStream.getText();
1063
1064 tokenAccumulator.add(new CommentToken(comment, getLocation()));
1065 }
1066
1067
1068
1069
1070 private void processTextBuffer(TemplateParserState state)
1071 {
1072 if (textBuffer.length() != 0)
1073 convertTextBufferToTokens(state);
1074
1075 textStartLocation = null;
1076 }
1077
1078 private void convertTextBufferToTokens(TemplateParserState state)
1079 {
1080 String text = textBuffer.toString();
1081
1082 textBuffer.setLength(0);
1083
1084 if (state.isCompressWhitespace())
1085 {
1086 text = compressWhitespaceInText(text);
1087
1088 if (InternalUtils.isBlank(text))
1089 return;
1090 }
1091
1092 addTokensForText(text);
1093 }
1094
1095
1096
1097
1098
1099
1100
1101
1102 private String compressWhitespaceInText(String text)
1103 {
1104 String linebreaksReduced = REDUCE_LINEBREAKS_PATTERN.matcher(text).replaceAll("\n");
1105
1106 return REDUCE_WHITESPACE_PATTERN.matcher(linebreaksReduced).replaceAll(" ");
1107 }
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119 private void addTokensForText(String text)
1120 {
1121 Matcher matcher = EXPANSION_PATTERN.matcher(text);
1122
1123 int startx = 0;
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134 while (matcher.find())
1135 {
1136 int matchStart = matcher.start();
1137
1138 if (matchStart != startx)
1139 {
1140 String prefix = text.substring(startx, matchStart);
1141 tokenAccumulator.add(new TextToken(prefix, textStartLocation));
1142 }
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164 String expression = matcher.group(1);
1165
1166
1167
1168 int openBraceCount = 1;
1169 int expressionEnd = expression.length();
1170 boolean inQuote = false;
1171 for (int i = 0; i < expression.length(); i++)
1172 {
1173 char c = expression.charAt(i);
1174
1175
1176 if (c == EXPANSION_STRING_DELIMITTER)
1177 {
1178 inQuote = !inQuote;
1179 continue;
1180 } else if (inQuote)
1181 {
1182 continue;
1183 } else if (c == CLOSE_BRACE)
1184 {
1185 openBraceCount--;
1186 if (openBraceCount == 0)
1187 {
1188 expressionEnd = i;
1189 break;
1190 }
1191 } else if (c == OPEN_BRACE)
1192 {
1193 openBraceCount++;
1194 }
1195 }
1196 if (expressionEnd < expression.length())
1197 {
1198
1199
1200 tokenAccumulator.add(new ExpansionToken(expression.substring(0, expressionEnd), textStartLocation));
1201
1202 startx = matcher.start(1) + expressionEnd + 1;
1203 } else
1204 {
1205 tokenAccumulator.add(new ExpansionToken(expression.trim(), textStartLocation));
1206
1207 startx = matcher.end();
1208 }
1209 }
1210
1211
1212
1213 if (startx < text.length())
1214 tokenAccumulator.add(new TextToken(text.substring(startx, text.length()),
1215 textStartLocation));
1216 }
1217
1218 static enum Version
1219 {
1220 T_5_0(5, 0), T_5_1(5, 1), T_5_3(5, 3), T_5_4(5, 4);
1221
1222 private int major;
1223 private int minor;
1224
1225
1226 private Version(int major, int minor)
1227 {
1228 this.major = major;
1229 this.minor = minor;
1230 }
1231
1232
1233
1234
1235
1236 public boolean sameOrEarlier(Version other)
1237 {
1238 if (other == null)
1239 return false;
1240
1241 if (this == other)
1242 return true;
1243
1244 return major <= other.major && minor <= other.minor;
1245 }
1246 }
1247
1248 }